package com.alpha.schema;

import com.alpha.core.utils.*;
import com.alpha.schema.beans.*;
import lombok.extern.slf4j.Slf4j;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.SAXException;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Schema 配置解析器
 *
 * @author chenruwan
 * @since 2022-01-01
 */
@Slf4j
public class SchemaParser {
    private SchemaParser() {
    }

    public static Schema parseSchema(String resource) {
        log.info("Parsing {}", resource);
        Schema schema = new Schema();

        Document document;
        try {
            SAXReader saxReader = new SAXReader();
            saxReader.setFeature("http" + "://apache.org/xml/features/disallow-doctype-decl", true);
            document = saxReader.read(FileUtil.file(resource));
        } catch (DocumentException | SAXException e) {
            log.info("Can Not Find File : {}", resource);
            return schema;
        }

        Element root = document.getRootElement();
        schema.setPackageName(root.attributeValue(SchemaConstants.ATTR_PACKAGE));
        schema.setReactive(Boolean.parseBoolean(root.attributeValue(SchemaConstants.ATTR_REACTIVE)));
        Assert.notEmpty(schema.getPackageName(), "root 的 package 属性不能为空");

        for (Element element : root.elements()) {
            if (SchemaConstants.TAG_INCLUDE.equals(element.getName())) {
                String dir = resource.substring(0, resource.lastIndexOf('/'));
                Schema include = parseSchema(PathUtil.combine(dir, element.getText()));
                schema.getChoices().addAll(include.getChoices());
                schema.getTables().addAll(include.getTables());
            } else if (SchemaConstants.TAG_TABLE.equals(element.getName())) {
                schema.getTables().add(parseTable(element));
            } else if (SchemaConstants.TAG_CHOICE.equals(element.getName())) {
                schema.getChoices().add(parseChoice(element));
            } else {
                unknownTag(element.getName());
            }
        }

        initRelations(schema);

        log.info("Schemas = {}", JsonUtil.toString(schema));
        return schema;
    }

    private static void initRelations(Schema schema) {
        for (Table table : schema.getTables()) {
            for (Relation relation : table.getRelations()) {
                if (relation.getName() == null) {
                    relation.setName(table.getName() + "_" + relation.getTo());
                }
                if (relation.getSingular() == null) {
                    relation.setSingular(table.getSingular() + "_" + schema.findTable(relation.getTo()).getSingular());
                }
            }
        }
    }

    private static Table parseTable(Element element) {
        int sequence = 1;
        Table table = parseAttrs(element, Table.class);
        for (Element child : element.elements()) {
            if (SchemaConstants.TAG_FIELD.equals(child.getName())) {
                table.getFields().add(parseField(child).setSequence(sequence++));
            } else if (SchemaConstants.TAG_FOREIGN.equals(child.getName())) {
                table.getForeigners().add(parseForeign(child).setSequence(sequence++));
            } else if (SchemaConstants.TAG_RELATION.equals(child.getName())) {
                table.getRelations().add(parseRelation(child));
            } else {
                unknownTag(child.getName());
            }
        }
        if (table.getAlias() == null) {
            table.setAlias(table.getName());
        }
        return table;
    }

    private static Choice parseChoice(Element element) {
        Choice choice = parseAttrs(element, Choice.class);

        for (Element child : element.elements()) {
            if (SchemaConstants.TAG_OPTION.equals(child.getName())) {
                choice.getOptions().add(parseOption(child));
            } else {
                unknownTag(child.getName());
            }
        }
        return choice;
    }

    private static Field parseField(Element element) {
        Field field = parseAttrs(element, Field.class);
        if (field.getFormat() != null) {
            field.setFormat(field.getFormat());
            return field;
        }

        if (SchemaConstants.TYPE_DATE.equals(field.getType())) {
            field.setFormat(DateUtil.NORM_DATE_PATTERN);
        } else if (SchemaConstants.TYPE_TIME.equals(field.getType())) {
            field.setFormat(DateUtil.NORM_TIME_PATTERN);
        } else if (SchemaConstants.TYPE_DATETIME.equals(field.getType())) {
            field.setFormat(DateUtil.NORM_DATETIME_PATTERN);
        }
        return field;
    }

    private static Foreign parseForeign(Element element) {
        return parseAttrs(element, Foreign.class);
    }

    private static Relation parseRelation(Element element) {
        Relation relation = parseAttrs(element, Relation.class);

        for (Element child : element.elements()) {
            if (SchemaConstants.TAG_FIELD.equals(child.getName())) {
                relation.getFields().add(parseField(child));
            } else {
                unknownTag(child.getName());
            }
        }
        return relation;
    }

    private static Option parseOption(Element element) {
        return parseAttrs(element, Option.class);
    }

    private static <T> T parseAttrs(Element element, Class<T> clazz) {
        T bean = BeanUtil.toBean(CollectionUtil.emptyMap(), clazz);
        for (Attribute attribute : element.attributes()) {
            try {
                String name = attribute.getName();
                if (SchemaConstants.ATTR_DEFAULT.equals(name)) {
                    name = "defVal";
                } else if (SchemaConstants.ATTR_NOT_NULL.equals(name)) {
                    name = "notNull";
                }
                Method method = clazz.getMethod("set" + StringUtil.toUpperCamel(name), String.class);
                method.invoke(bean, attribute.getValue());
            } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
                log.error("无法识别的属性 {}", attribute.getName());
                Assert.isTrue(false, "无法识别的属性 {}", attribute.getName());
            }
        }
        return bean;
    }

    private static void unknownTag(String tag) {
        log.error("无法识别的标记 {}", tag);
        Assert.isTrue(false, "无法识别的标记 {}", tag);
    }
}
